// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;

namespace Microsoft.Internal
{
    internal static class GenerationServices
    {
        // Type.GetTypeFromHandle
        private static readonly MethodInfo _typeGetTypeFromHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle");


        // typeofs are pretty expensive, so we cache them statically
        private static readonly Type TypeType = typeof(System.Type);
        private static readonly Type StringType = typeof(System.String);
        private static readonly Type CharType = typeof(System.Char);
        private static readonly Type BooleanType = typeof(System.Boolean);
        private static readonly Type ByteType = typeof(System.Byte);
        private static readonly Type SByteType = typeof(System.SByte);
        private static readonly Type Int16Type = typeof(System.Int16);
        private static readonly Type UInt16Type = typeof(System.UInt16);
        private static readonly Type Int32Type = typeof(System.Int32);
        private static readonly Type UInt32Type = typeof(System.UInt32);
        private static readonly Type Int64Type = typeof(System.Int64);
        private static readonly Type UInt64Type = typeof(System.UInt64);
        private static readonly Type DoubleType = typeof(System.Double);
        private static readonly Type SingleType = typeof(System.Single);
        private static readonly Type ObjectType = typeof(System.Object);
        private static readonly Type GenericIDictionaryType = typeof(System.Collections.Generic.IDictionary<,>);
        private static readonly Type GenericDictionaryType = typeof(System.Collections.Generic.Dictionary<,>);
        private static readonly Type IEnumerableTypeofT = typeof(System.Collections.Generic.IEnumerable<>);
        private static readonly Type IEnumerableType = typeof(System.Collections.IEnumerable);
        private static readonly Type DelegateType = typeof(System.Delegate);
        private static readonly Type GeneratedDelegateType = typeof(GeneratedDelegate);
        private static readonly Type GenericGeneratedDelegateType = typeof(GeneratedDelegate<>);


        /// Generates the code that loads the supplied value on the stack
        /// This is not as simple as it seems, as different instructions need to be generated depending
        /// on its type.
        /// We support:
        /// 1. All primitive types
        /// 2. Strings
        /// 3. Enums
        /// 4. typeofs
        /// 5. nulls
        /// 6. Dictionaries of (string, object) recursively containing all of the above
        /// 7. Enumerables
        /// 8. Delegates on static functions or any of the above
        /// Everything else cannot be represented as literals
        /// <param name="ilGenerator"></param>
        /// <param name="item"></param>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        internal static CompositionResult LoadValue(this ILGenerator ilGenerator, object value)
        {
            Assumes.NotNull(ilGenerator);
            CompositionResult result = CompositionResult.SucceededResult;

            //
            // Get nulls out of the way - they are basically typeless, so we just load null
            //
            if (value == null)
            {
                return ilGenerator.LoadNull();
            }

            //
            // Prepare for literal loading - decide whether we should box, and handle enums properly
            //
            Type valueType = value.GetType();
            object rawValue = value;
            if (valueType.IsEnum)
            {
                // enums are special - we need to load the underlying constant on the stack
                rawValue = Convert.ChangeType(value, Enum.GetUnderlyingType(valueType), null);
                valueType = rawValue.GetType();
            }

            //
            // Generate IL depending on the valueType - this is messier than it should ever be, but sadly necessary
            //
            Type dictionaryKeyType;
            Type dictionaryValueType;
            if (GenerationServices.TryGetDictionaryElementType(valueType, out dictionaryKeyType, out dictionaryValueType))
            {
                result = result.MergeResult(ilGenerator.LoadDictionary(rawValue, dictionaryKeyType, dictionaryValueType));
            }
            else if (valueType == GenerationServices.StringType)
            {
                // we need to check for strings before enumerables, because strings are IEnumerable<char>
                result = result.MergeResult(ilGenerator.LoadString((string)rawValue));
            }
            else if (GenerationServices.TypeType.IsAssignableFrom(valueType))
            {
                result = result.MergeResult(ilGenerator.LoadTypeOf((Type)rawValue));
            }
            else if(GenerationServices.DelegateType.IsAssignableFrom(valueType))
            {
                result = result.MergeResult(ilGenerator.LoadDelegate((Delegate)rawValue));
            }
            else if (GenerationServices.GeneratedDelegateType.IsAssignableFrom(valueType))
            {
                result = result.MergeResult(ilGenerator.LoadDelegate((GeneratedDelegate)rawValue));
            }
            else if (GenerationServices.IEnumerableType.IsAssignableFrom(valueType))
            {
                // NOTE : strings and dictionaries are also enumerables, but we have already handled those
                result = result.MergeResult(ilGenerator.LoadEnumerable((IEnumerable) rawValue));
            }
            else if (
                (valueType == GenerationServices.CharType) ||
                (valueType == GenerationServices.BooleanType) ||
                (valueType == GenerationServices.ByteType) ||
                (valueType == GenerationServices.SByteType) ||
                (valueType == GenerationServices.Int16Type) ||
                (valueType == GenerationServices.UInt16Type) ||
                (valueType == GenerationServices.Int32Type)
                )
            {
                // NOTE : Everything that is 32 bit or less uses ldc.i4. We need to pass int32, even if the actual types is shorter - this is IL memory model
                // direct casting to (int) won't work, because the value is boxed, thus we need to use Convert.
                // Sadly, this will not work for all cases - namely large uint32 - because they can't semantically fit into 32 signed bits
                // We have a special case for that next
                result = result.MergeResult(ilGenerator.LoadInt((int)Convert.ChangeType(rawValue, typeof(int), CultureInfo.InvariantCulture)));
            }
            else if (valueType == GenerationServices.UInt32Type)
            {
                // NOTE : This one is a bit tricky. Ldc.I4 takes an Int32 as an argument, although it really treats it as a 32bit number
                // That said, some UInt32 values are larger that Int32.MaxValue, so the Convert call above will fail, which is why 
                // we need to treat this case individually and cast to uint, and then - unchecked - to int.
                result = result.MergeResult(ilGenerator.LoadInt(unchecked((int)((uint)rawValue))));
            }
            else if (valueType == GenerationServices.Int64Type)
            {
                result = result.MergeResult(ilGenerator.LoadLong((long)rawValue));
            }
            else if (valueType == GenerationServices.UInt64Type)
            {
                // NOTE : This one is a bit tricky. Ldc.I8 takes an Int64 as an argument, although it really treats it as a 64bit number
                // That said, some UInt64 values are larger that Int64.MaxValue, so the direct case we use above (or Convert, for that matter)will fail, which is why
                // we need to treat this case individually and cast to ulong, and then - unchecked - to long.
                result = result.MergeResult(ilGenerator.LoadLong(unchecked((long)((ulong)rawValue))));
            }
            else if (valueType == GenerationServices.SingleType)
            {
                result = result.MergeResult(ilGenerator.LoadFloat((float)rawValue));
            }
            else if (valueType == GenerationServices.DoubleType)
            {
                result = result.MergeResult(ilGenerator.LoadDouble((double)rawValue));
            }
            else
            {
                result = result.MergeIssue(CompositionIssue.CreateIssueAsError(
                            CompositionIssueId.UnsupportedCachedValue,
                            Strings.UnsupportedCachedValue,
                            value.GetType()));

                // Make sure the IL is balanced - generate the ldnull instead
                ilGenerator.LoadNull();
            }

            return result;
        }

        private static CompositionResult LoadDictionary(this ILGenerator ilGenerator, object dictionary, Type keyType, Type valueType)
        {
            Assumes.NotNull(ilGenerator);
            Assumes.NotNull(dictionary);
            Assumes.NotNull(keyType);
            Assumes.NotNull(valueType);

            // this is the most common case, so we optimize for it so as to avoid the self-reflection call
            if ((keyType == GenerationServices.StringType) && (valueType == GenerationServices.ObjectType))
            {
                return ilGenerator.LoadGenericDictionary((IDictionary<string, object>)dictionary);
            }

            // UGLY!!!
            return (CompositionResult)(typeof(GenerationServices).GetMethod("LoadGenericDictionary", BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(keyType, valueType)
                .Invoke(null, new object[] { ilGenerator, dictionary }));
        }

        private static CompositionResult LoadGenericDictionary<TKey, TValue>(this ILGenerator ilGenerator, IDictionary<TKey, TValue> dictionary)
        {
            Assumes.NotNull(ilGenerator);
            Assumes.NotNull(dictionary);

            CompositionResult result = CompositionResult.SucceededResult;
            Type keyType = GenerationServices.NormalizeCollectionElementType(typeof(TKey));
            Type valueType = GenerationServices.NormalizeCollectionElementType(typeof(TValue));
            Type idictionaryType = GenerationServices.GenericIDictionaryType.MakeGenericType(keyType, valueType);
            Type dictionaryType = GenerationServices.GenericDictionaryType.MakeGenericType(keyType, valueType);
            MethodInfo idictionaryAddMethod = idictionaryType.GetMethod("Add", new Type[] { keyType, valueType });

            //
            // IDictionary<TKey, TValue> metadata = new Dictionary<TKey, TValue>(capacity)
            //
            LocalBuilder generatedDictionaryLocal = ilGenerator.DeclareLocal(idictionaryType);
            ilGenerator.LoadInt(dictionary.Count);
            ilGenerator.Emit(OpCodes.Newobj, dictionaryType.GetConstructor(new Type[] { typeof(int) }));
            ilGenerator.Emit(OpCodes.Stloc, generatedDictionaryLocal);

            // 
            // Generate a sequence of "Add" statements
            //
            
            foreach (KeyValuePair<TKey, TValue> dictionaryItem in dictionary)
            {
                // metadata.Add(key, value)
                ilGenerator.Emit(OpCodes.Ldloc, generatedDictionaryLocal);

                result = result.MergeResult(ilGenerator.LoadValue(dictionaryItem.Key));
                if (GenerationServices.IsBoxingRequiredForValue(dictionaryItem.Key) && !keyType.IsValueType)
                {
                    ilGenerator.Emit(OpCodes.Box, dictionaryItem.Key.GetType());
                }

                result = result.MergeResult(ilGenerator.LoadValue(dictionaryItem.Value));
                if (GenerationServices.IsBoxingRequiredForValue(dictionaryItem.Value) && !valueType.IsValueType)
                {
                    ilGenerator.Emit(OpCodes.Box, dictionaryItem.Value.GetType());
                }

                ilGenerator.EmitCall(OpCodes.Callvirt, idictionaryAddMethod, null);
            }

            // 
            // Load the local on stack and exit
            //
            ilGenerator.Emit(OpCodes.Ldloc, generatedDictionaryLocal);
            return result;

        }

        private static CompositionResult LoadEnumerable(this ILGenerator ilGenerator, IEnumerable enumerable)
        {
            Assumes.NotNull(ilGenerator);
            Assumes.NotNull(enumerable);

            CompositionResult result = CompositionResult.SucceededResult;

            // We load enumerable as an array - this is the most compact and efficient way of representing it
            Type elementType = null;
            Type closedType = null;
            if (ReflectionServices.TryGetGenericInterfaceType(enumerable.GetType(), GenerationServices.IEnumerableTypeofT, out closedType))
            {
                elementType = closedType.GetGenericArguments()[0];
            }
            else
            {
                elementType = typeof(object);
            }
            elementType = GenerationServices.NormalizeCollectionElementType(elementType);

            //
            // elem[] array = new elem[<enumerable.Count()>]
            //
            Type generatedArrayType = elementType.MakeArrayType();
            LocalBuilder generatedArrayLocal = ilGenerator.DeclareLocal(generatedArrayType);

            ilGenerator.LoadInt(enumerable.Cast<object>().Count());
            ilGenerator.Emit(OpCodes.Newarr, elementType);
            ilGenerator.Emit(OpCodes.Stloc, generatedArrayLocal);
            
            int index = 0;
            foreach (object value in enumerable)
            {
                //
                //array[<index>] = value;
                //
                ilGenerator.Emit(OpCodes.Ldloc, generatedArrayLocal);
                ilGenerator.LoadInt(index);
                result = result.MergeResult(ilGenerator.LoadValue(value));
                if (GenerationServices.IsBoxingRequiredForValue(value) && !elementType.IsValueType)
                {
                    ilGenerator.Emit(OpCodes.Box, value.GetType());
                }
                ilGenerator.Emit(OpCodes.Stelem, elementType);
                index++;
            }

            ilGenerator.Emit(OpCodes.Ldloc, generatedArrayLocal);

            return result;
        }


        internal static bool IsLoadable(object value)
        {
            Type valueType = (value != null) ? value.GetType() : null;
            return (
                (valueType == null) ||
                (valueType.IsPrimitive) ||
                valueType.IsEnum ||
                (value is string) ||
                (value is Type)||
                (value is Delegate) ||
                (valueType == GenerationServices.GeneratedDelegateType) ||
                GenerationServices.IsLoadableDictionaryType(valueType) ||
                (value is IEnumerable) 
                );
        }

        private static Type NormalizeCollectionElementType(Type type)
        {
            // handle generated delegates
            if (GenerationServices.GeneratedDelegateType.IsAssignableFrom(type))
            {
                // it MUST be GeneratedDelegate<T>. If not - we didn't do out job right
                Assumes.IsTrue(type.GetGenericTypeDefinition() == GenerationServices.GenericGeneratedDelegateType);
                return type.GetGenericArguments()[0];
            }
            else if (GenerationServices.IEnumerableType.IsAssignableFrom(type) && type != GenerationServices.StringType)
            {
                // the element is IEnumerable. we need to normalize it to be literally IEnumerable
                Type closedType = null;
                if (ReflectionServices.TryGetGenericInterfaceType(type, GenerationServices.IEnumerableTypeofT, out closedType))
                {
                    return GenerationServices.IEnumerableTypeofT.MakeGenericType(GenerationServices.NormalizeCollectionElementType(closedType.GetGenericArguments()[0]));
                }
                else
                {
                    return typeof(IEnumerable<object>);
                }
            }
            else
            {
                return type;
            }
        }


        private static bool IsLoadableDictionaryType(Type type)
        {
            Assumes.NotNull(type);
            Type keyType = null;
            Type valueType = null;

            return GenerationServices.TryGetDictionaryElementType(type, out keyType, out valueType);
        }

        private static bool TryGetDictionaryElementType(Type type, out Type keyType, out Type valueType)
        {
            Assumes.NotNull(type);
            keyType = null;
            valueType = null;

            if (!type.IsGenericType)
            {
                return false;
            }

            Type genericType = type.GetGenericTypeDefinition();
            Assumes.NotNull(genericType);

            Type closedInterfaceType = type.GetInterface(GenerationServices.GenericIDictionaryType.Name, false);
            if (closedInterfaceType == null)
            {
                return false;
            }

            keyType = closedInterfaceType.GetGenericArguments()[0];
            valueType = closedInterfaceType.GetGenericArguments()[1];

            return GenerationServices.IsLoadable(keyType) && GenerationServices.IsLoadable(valueType);
        }

        private static bool IsBoxingRequiredForValue(object value)
        {
            if (value == null)
            {
                return false;
            }
            else
            {
                return value.GetType().IsValueType;
            }
        }


        private static CompositionResult LoadNull(this ILGenerator ilGenerator)
        {
            ilGenerator.Emit(OpCodes.Ldnull);
            return CompositionResult.SucceededResult;
        }

        private static CompositionResult LoadString(this ILGenerator ilGenerator, string s)
        {
            Assumes.NotNull(ilGenerator);
            if (s == null)
            {
                return GenerationServices.LoadNull(ilGenerator);
            }
            else
            {
                ilGenerator.Emit(OpCodes.Ldstr, s);
            }
            return CompositionResult.SucceededResult;
        }


        private static CompositionResult LoadInt(this ILGenerator ilGenerator, int value)
        {
            Assumes.NotNull(ilGenerator);
            ilGenerator.Emit(OpCodes.Ldc_I4, value);
            return CompositionResult.SucceededResult;
        }

        private static CompositionResult LoadLong(this ILGenerator ilGenerator, long value)
        {
            Assumes.NotNull(ilGenerator);
            ilGenerator.Emit(OpCodes.Ldc_I8, value);
            return CompositionResult.SucceededResult;
        }

        private static CompositionResult LoadFloat(this ILGenerator ilGenerator, float value)
        {
            Assumes.NotNull(ilGenerator);
            ilGenerator.Emit(OpCodes.Ldc_R4, value);
            return CompositionResult.SucceededResult;
        }

        private static CompositionResult LoadDouble(this ILGenerator ilGenerator, double value)
        {
            Assumes.NotNull(ilGenerator);
            ilGenerator.Emit(OpCodes.Ldc_R8, value);
            return CompositionResult.SucceededResult;
        }

        private static CompositionResult LoadTypeOf(this ILGenerator ilGenerator, Type type)
        {
            Assumes.NotNull(ilGenerator);
            //typeofs() translate into ldtoken and Type::GetTypeFromHandle call
            ilGenerator.Emit(OpCodes.Ldtoken, type);
            ilGenerator.EmitCall(OpCodes.Call, GenerationServices._typeGetTypeFromHandleMethod, null);
            return CompositionResult.SucceededResult;
        }

        private static CompositionResult LoadDelegate(this ILGenerator ilGenerator, Delegate _delegate)
        {
            CompositionResult result = CompositionResult.SucceededResult;
            Assumes.NotNull(ilGenerator);
            Assumes.NotNull(_delegate);

            result = result.MergeResult(ilGenerator.LoadValue(_delegate.Target));
            ilGenerator.Emit(OpCodes.Ldftn, _delegate.Method);
            ilGenerator.Emit(OpCodes.Newobj, _delegate.GetType().GetConstructor(new Type[] { typeof(object), typeof(IntPtr) }));

            return result;            
        }

        private static CompositionResult LoadDelegate(this ILGenerator ilGenerator, GeneratedDelegate _delegate)
        {
            CompositionResult result = CompositionResult.SucceededResult;
            Assumes.NotNull(ilGenerator);
            Assumes.NotNull(_delegate);
            Assumes.NotNull(_delegate.DelegateType);
            Assumes.NotNull(_delegate.Method);

            result = result.MergeResult(ilGenerator.LoadValue(_delegate.Target));
            ilGenerator.Emit(OpCodes.Ldftn, _delegate.Method);
            ilGenerator.Emit(OpCodes.Newobj, _delegate.DelegateType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) }));

            return result;
        }

    }
}
